package job

import (
	"bigdevops/src/common"
	"bigdevops/src/config"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"go.uber.org/zap"
	"os"
	"os/exec"
	"strings"
	"sync"
	"syscall"
	"time"
)

type Task struct {
	// 加锁的目的是 可能有多个人处理，或者多个情况同时发生
	sync.Mutex
	// 任务id
	Id int `json:"id"`

	ScriptType string `json:"scriptType"`
	// 执行账号 root devops
	Account string `json:"account"`
	// 脚本内容 来自与模板 从server端获取的
	ScriptContent string `json:"-"`
	//	参数 先假定以空格分割
	Args string `json:"args"`

	// 状态
	Status             string `json:"status"`
	ExecTimeoutSeconds int    `json:"execTimeoutSeconds"`

	ScriptPath string `json:"scriptPath"`

	// 需求是 多个任务的结果 也保存到本地，目的是进行问题排查
	TaskDir string              `json:"taskDir"`
	Sc      *config.AgentConfig `json:"-"`
	Cmd     *exec.Cmd           `json:"-"`
	// 结果
	Stdout, Stderr bytes.Buffer `json:"-"`
}

var (
	mockScriptContentNoArgs = `
kubectl get node 
`

	mockScriptContentWithArgs = `
kubectl get node $1
`

	mockScriptContentSleep = `
date 
echo hello
sleep 100
`

	mockScriptContentNoRoot = `
pwd
ls -l
echo nyy
date > /root/abc

`

	mockScriptContentPythonNoArgs = `#!/usr/bin/env python3
import time

print("哈哈.sleep1000")
time.sleep(1000)
`

	mockScriptContentPythonWithArgs = `#!/usr/bin/env python3
import sys


if __name__ == "__main__":
    args = sys.argv
    print("参数", args)
    print("加法结果", int(args[1]) + 10)

`
)

func MockTaskRun(sc *config.AgentConfig) {

	// t1 代表 没有参数的短任务
	t1 := &Task{
		Account:       "root",
		ScriptContent: mockScriptContentNoArgs,
		Id:            1,
		Sc:            sc,
	}
	// t1 代表 有参数的短任务
	t2 := &Task{
		Account:       "root",
		ScriptContent: mockScriptContentWithArgs,
		Args:          "k8s-master01",
		Id:            2,
		Sc:            sc,
	}

	// t1 代表 sleep
	t3 := &Task{
		Account:       "root",
		ScriptContent: mockScriptContentSleep,
		Id:            3,
		Sc:            sc,
	}

	// t4代表非root用户
	t4 := &Task{
		Account:       "nyy",
		ScriptContent: mockScriptContentNoRoot,
		Id:            4,
		Sc:            sc,
	}

	// t4代表 root python 长时间
	t5 := &Task{
		Account:       "root",
		ScriptContent: mockScriptContentPythonNoArgs,
		ScriptType:    common.AGENT_TASK_SCRIPT_TYPE_PYTHON,
		Id:            5,
		Sc:            sc,
	}

	t6 := &Task{
		Account:       "nyy",
		ScriptContent: mockScriptContentPythonWithArgs,
		ScriptType:    common.AGENT_TASK_SCRIPT_TYPE_PYTHON,
		Id:            6,
		Sc:            sc,
		Args:          "100",
	}

	t7 := &Task{
		Account:       "root",
		ScriptContent: mockScriptContentPythonWithArgs,
		ScriptType:    common.AGENT_TASK_SCRIPT_TYPE_PYTHON,
		Id:            7,
		Sc:            sc,
		Args:          "100",
	}
	ts := []*Task{
		t1,
		t2,
		t3,
		t4,
		t5,
		t6,
		t7,
	}
	for _, t := range ts {
		t := t
		t.SetTaskDir()
		t.Start()
	}

}

func (t *Task) SetTaskDir() {
	t.TaskDir = fmt.Sprintf("%s/%d", t.Sc.JobExecC.MetaDir, t.Id)
	if t.ScriptType == "" {
		t.ScriptType = common.AGENT_TASK_SCRIPT_TYPE_SHELL
	}
}

func (t *Task) SetStatus(status string) {
	t.Lock()
	defer t.Unlock()
	t.Status = status
}

func (t *Task) GetStdout() string {
	t.Lock()
	defer t.Unlock()
	return t.Stdout.String()
}

func (t *Task) GetStderr() string {
	t.Lock()
	defer t.Unlock()
	return t.Stderr.String()
}

func (t *Task) GetStatus() string {
	t.Lock()
	defer t.Unlock()
	return t.Status
}

// 任务开始前的准备工作
func (t *Task) Prepare() (err error) {
	// 提前准备这个目录
	//

	err = os.MkdirAll(t.TaskDir, os.ModePerm)
	if err != nil {
		t.Sc.Logger.Error("任务开始前创建元信息目录失败",

			zap.Error(err),
			zap.Any("任务id", t.Id),
		)
		return
	}

	// 要把 脚本内容给它写入
	// 0777的目的是 还会有其他用户可能执行
	scriptPath := fmt.Sprintf("%s/script", t.TaskDir)
	t.ScriptPath = scriptPath
	err = os.WriteFile(scriptPath, []byte(t.ScriptContent), 0777)
	if err != nil {
		t.Sc.Logger.Error("任务开始前写入脚本失败",

			zap.Error(err),
			zap.Any("任务id", t.Id),
		)
		return
	}

	// 把元信息写入
	metaJson, err := json.Marshal(t)
	if err != nil {
		t.Sc.Logger.Error("任务开始前写入元信息文件json错误",

			zap.Error(err),
			zap.Any("任务id", t.Id),
		)
		return
	}

	err = os.WriteFile(fmt.Sprintf("%s/meta.json", t.TaskDir), metaJson, 0666)
	if err != nil {
		t.Sc.Logger.Error("任务开始前写入元信息文件json失败",

			zap.Error(err),
			zap.Any("任务id", t.Id),
		)
		return
	}

	startTime := time.Now().Format("2006-01-02 15:04:05")
	err = os.WriteFile(fmt.Sprintf("%s/startTime", t.TaskDir), []byte(startTime), 0666)
	if err != nil {
		t.Sc.Logger.Error("任务开始前写入启动时间文件失败",

			zap.Error(err),
			zap.Any("任务id", t.Id),
		)
		return
	}

	return nil
}

// 紧急kill的方法
func (t *Task) Kill() {
	if t.Cmd == nil {
		return
	}
	t.Sc.Logger.Info("任务准备kill了",
		zap.Any("任务id", t.Id),
		zap.Any("进程id", t.Cmd.Process.Pid),
	)
	syscall.Kill(-t.Cmd.Process.Pid, syscall.SIGKILL)

}

// 执行逻辑 直接context版本
func (t *Task) Start() error {

	// 在start之前 先 执行prepare
	err := t.Prepare()
	if err != nil {
		t.Sc.Logger.Error("任务准备失败",
			zap.Error(err),
			zap.Any("任务id", t.Id),
		)
		return err
	}

	t.Sc.Logger.Info("任务开始前的准备工作已完成",
		zap.Any("任务id", t.Id),
	)
	if t.ExecTimeoutSeconds == 0 {
		t.ExecTimeoutSeconds = t.Sc.JobExecC.ExecTimeoutSeconds
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(t.ExecTimeoutSeconds)*time.Second)
	// 这里的cancel 会在这个函数返回的时候直接执行
	// 所以 下面的结果处理 不能异步做，因为会导致 无法开始执行就被kill
	defer cancel()
	// 强制它 bash -c ，规避  kill 进程组用法 引入的syscall  在window 上的编译问题
	// new cmd 对象
	// 判断用户
	cmdName := ""
	cmdArgs := []string{}

	// 为了解决 bash -c 传参问题
	// 需要将 参数和脚本放一起
	scriptWithArgs := fmt.Sprintf("%s %s", t.ScriptPath, t.Args)

	// 先不考虑python问题 只考虑 account
	if t.Account == "root" {
		// sh 或者bash
		//cmdName = "sh"
		cmdName = t.Sc.JobExecC.BashBinPath
		cmdArgs = []string{

			"-c",
			scriptWithArgs,
		}
	} else {
		cmdName = "su"
		cmdArgs = []string{

			t.Account,
			"-c",
			scriptWithArgs,
		}
	}

	cmd := exec.CommandContext(ctx, cmdName, cmdArgs...)
	//cmd := exec.CommandContext(ctx, "bash", "-c", "echo hello && sleep 1200")
	t.Sc.Logger.Info("任务的命令打印",
		zap.Any("任务id", t.Id),
		zap.Any("任务脚本类型", t.ScriptType),
		zap.Any("命令cmd-Args", cmd.Args),
		zap.Any("命令cmd-Env", cmd.Env),
		zap.Any("命令cmd-Dir", cmd.Dir),
		zap.Any("命令cmd-Path", cmd.Path),
	)
	cmd.Stdout = &t.Stdout
	cmd.Stderr = &t.Stderr

	// 后面我们可能会启用 kill 进程组用法
	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
	go func() {
		select {
		case <-ctx.Done():
			// cmd.Process.Kill()
			syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
		}
	}()

	t.Cmd = cmd
	// 这里不使用 CombinedOutput的目的是  CombinedOutput里面又会去设置 stdout stderr
	t.SetStatus(common.AGENT_TASK_STATUS_RUNNING)
	err = cmd.Start()

	if err != nil {

		t.Sc.Logger.Error("任务Start失败",
			zap.Error(err),
			zap.Any("任务id", t.Id),
		)
		return err
	}
	// 同步处理结果
	t.runProcess()
	return nil
}

// 设置状态，等待结果，写入文件
func (t *Task) runProcess() {
	err := t.Cmd.Wait()
	if err != nil {
		if strings.Contains(err.Error(), "signal: killed") {
			t.SetStatus(common.AGENT_TASK_STATUS_KILLED)
			t.Sc.Logger.Error("任务被人为 或触发超时kill",
				zap.Error(err),
				zap.Any("任务id", t.Id),
			)

		} else {
			t.SetStatus(common.AGENT_TASK_STATUS_FAILED)
			t.Sc.Logger.Error("任务执行出错",
				zap.Error(err),
				zap.Any("任务id", t.Id),
			)
		}
	} else {
		// 执行成功
		t.SetStatus(common.AGENT_TASK_STATUS_SUCCESS)
		t.Sc.Logger.Info("任务执行成功",
			zap.Error(err),
			zap.Any("任务id", t.Id),
		)

	}

	// 处理任务的结果
	t.persistResult()
}

// 任务结果落盘 ，保存到文件
// stdout stderr 分别读取出来 写入对应的文件
func (t *Task) persistResult() {

	stdOutPath := fmt.Sprintf("%s/stdout", t.TaskDir)
	stdErrPath := fmt.Sprintf("%s/stderr", t.TaskDir)
	statusPath := fmt.Sprintf("%s/status", t.TaskDir)

	// 写标准输出
	err := common.WriteFileWithString(stdOutPath, t.GetStdout())
	if err != nil {
		t.Sc.Logger.Error("任务执行完结果写入文件出错",
			zap.Error(err),
			zap.Any("任务id", t.Id),
			zap.Any("标准输出", stdOutPath),
		)
	}
	// 标准错误

	err = common.WriteFileWithString(stdErrPath, t.GetStderr())
	if err != nil {
		t.Sc.Logger.Error("任务执行完结果写入文件出错",
			zap.Error(err),
			zap.Any("任务id", t.Id),
			zap.Any("标准错误", stdErrPath),
		)
	}

	// 写最终的状态
	err = common.WriteFileWithString(statusPath, t.GetStatus())
	if err != nil {
		t.Sc.Logger.Error("任务执行完结果写入文件出错",
			zap.Error(err),
			zap.Any("任务id", t.Id),
			zap.Any("状态结果", statusPath),
		)
	}

	return

}
